package org.vaadin.nf4v.core;

import com.vaadin.Application;
import com.vaadin.service.ApplicationContext;
import com.vaadin.ui.Window;
import org.vaadin.nf4v.core.config.ContextConfig;
import org.vaadin.nf4v.core.config.ContextConfigurationException;
import org.vaadin.nf4v.core.config.NavigationConfig;
import org.vaadin.nf4v.core.handler.WindowFactory;
import org.vaadin.nf4v.core.navigation.NavigationManager;
import org.vaadin.nf4v.core.navigation.event.NavigationState;
import org.vaadin.nf4v.core.util.NavigationFlash;
import org.vaadin.nf4v.ext.runtime.afternav.AfterNavigationListener;
import org.vaadin.nf4v.ext.runtime.beforenav.BeforeNavigationListener;

import javax.servlet.http.HttpServletRequest;

/**
 * Application that control NavigableWindow(s) and manages browser tabs.
 * Recognizes initial application load and can correctly show selected view without loading home view first.
 * <p/>
 * Inspired by navigator7 (http://vaadin.com/directory#addon/navigator7) by John Rizzo.
 *
 * @author xvik
 */
public abstract class NavigableApplication extends Application implements ApplicationContext.TransactionListener {

    private static ThreadLocal<NavigableApplication> currentApplication = new ThreadLocal<NavigableApplication>();
    private static ThreadLocal<NavigableWindow> currentNavigableWindow = new ThreadLocal<NavigableWindow>();
    // Trick until further version of Vaadin. See comment in transactionListener below
    private static ThreadLocal<String> initialUriFragment = new ThreadLocal<String>();
    private static ThreadLocal<HttpServletRequest> currentRequest = new ThreadLocal<HttpServletRequest>();
    private NavigationManager navigationManager = new NavigationManager();
    private NavigationFlash flash = new NavigationFlash();
    // base path is used to create link for context change
    private static String servletContext;

    /**
     * Application should implement this method in order to provide navigation configuration.
     * Extend NavigationConfigBuilder to build configuration (use getNavigationConfig() on builder to get configuration instance)
     * Method called only on application initialization.
     * Application developer can choose configuration strategy: create configuration on each application initialization
     * or "cache" it somewhere and reuse configuration instance.
     * <p/>
     * The simplest approach is to use "singleton" pattern for configuration.
     *
     * @return application navigation configuration
     */
    public abstract NavigationConfig getNavigationConfig();

    /**
     * Application developer shouldn't override this method.
     * Usually method is used to register application windows, but since framework handles windows itself it is not necessary anymore.
     * Use onApplicationStart() instead to perform application initialization.
     * <p/>
     * If, for some reason, you need to override it, don't forget to call super(), or at least initInternal().
     */
    @Override
    public void init() {
        boolean currentWasNull = false;  // During initialization, we trigger code that might needs getCurrent(), but TransactionListener has not been set yet and will not be called.
        if (getCurrent() == null) {
            currentApplication.set(this);
            currentWasNull = true;
        }

        initInternal();
        onApplicationStart();

        if (currentWasNull) {
            currentApplication.remove();
        }
    }

    /**
     * Initializes internal framework mechanisms and registers main window.
     */
    protected void initInternal() {
        if (getNavigationConfig() == null) {
            throw new ContextConfigurationException("Please implement correctly 'getNavigationConfig()' method " +
                    "in order to provide not null configuration");
        }
        getContext().addTransactionListener(this);
        NavigationManager.addApplicationNavigationListener(flash,
                NavigationState.AFTER_NAVIGATION);
        intiDefaultExtensions();

        setMainWindow(createWindow(getNavigationConfig().getMainContext(),
                getNavigationConfig().getMainContext()));
    }

    /**
     * Default runtime extensions initialization.
     * Note usually extensions are separated into two parts: configuration and runtime.
     * Configuration parts are registered in NavigationConfigProcessor#initDefaultExtensions()
     * <p/>
     * Override method to change default extensions.
     */
    protected void intiDefaultExtensions() {
        // @AfterNavigation page annotation support
        NavigationManager.addApplicationNavigationListener(new AfterNavigationListener(),
                NavigationState.PAGE_PARAMETERS_CHANGE, NavigationState.AFTER_NAVIGATION);
        // @BeforeNavigation page annotation support
        NavigationManager.addApplicationNavigationListener(new BeforeNavigationListener(),
                NavigationState.PAGE_NAVIGATION, NavigationState.PAGE_PARAMETERS_CHANGE,
                NavigationState.PAGE_INSTANTIATED, NavigationState.BEFORE_NAVIGATION);
    }

    /**
     * Put all initialization logic here (e.g. registering of global navigation listeners).
     * This place should be used instead of usual init().
     */
    protected abstract void onApplicationStart();

    /**
     * {@inheritDoc}
     * Resolves window object by window name.
     * Detects browser tabs and creates separate window versions (currently only for main window).
     * <p/>
     * If current request is initial one or the first one for different context shows home page.
     */
    @Override
    public Window getWindow(String name) {
        if (!isRunning()) {
            return null;
        }

        // todo investigate: strange bug - after context change window object isn't stored
        // todo after context switching and clicking browser back window gets hanged (bug occurred on chrome 7, but with dev console it disappears)

        String context = getNavigationConfig().resolveNavigationWindowContext(name);

        boolean newWindow = false;
        Window window = super.getWindow(name);
        if (window == null) {
            newWindow = true;
            // New case where we create one instance of the main window per browser window/tab that the user opens in the same HttpSession (= in the same Application instance).
            ///// Is it a request for a new main window (from another browser tab, for the same user/session) ?
            ///// If the name is like MainWindowName_01234567890 (where the number is random), then it is a request for a new instance of main window.
            ///// else, it's noise (floating window?)


            if (context == null) { // Already happened during debugging with Vaadin 6.0 or 6.1 (don't remember)
                return null;
            }
            window = createWindow(context, name);

            addWindow(window);
        }

        if (context != null) {
            currentNavigableWindow.set((NavigableWindow) window);
            // for "".equals(initialUriFragment.get())
            // SEE: http://vaadin.com/forum/-/message_boards/message/57240
            //   Probably to be removed with Vaadin 7 and the notion of application level window.
            // This case is different from null. If "", then it's for the home page.
            getCurrentNavigableWindow().getNavigator().handleContextActivation(newWindow || "".equals(initialUriFragment.get()));
        }

        return window;
    }

    /**
     * Creates window instance for current context with provided window name.
     * Uses WindowFactory for window object instantiation.
     * Sets window name (note window name may be different from context name,
     * because this name may indicate different browser tab).
     * Sets window theme (if configured)
     *
     * @param context current context
     * @param name    window name
     * @return window instance
     */
    @SuppressWarnings("unchecked")
    protected Window createWindow(String context, String name) {
        ContextConfig config = getNavigationConfig().getContextConfig(context);
        Window window = (Window) getNavigationConfig().getHandler(WindowFactory.class, context)
                .createWindow(config.getWindowClass(), name);
        window.setName(name);
        if (config.getTheme() != null) {
            window.setTheme(config.getTheme());
        }
        return window;
    }

    /**
     * It is better to override this in your application class (note, that type conversion will be automatic):
     * <pre>
     * public static YourApplicationClass T getCurrent() {
     *   return NavigableApplication.getCurrent();
     * }
     * </pre>
     *
     * @return current application instance
     */
    @SuppressWarnings("unchecked")
    public static <T extends NavigableApplication> T getCurrent() {
        return (T) currentApplication.get();
    }

    /**
     * @return the current navigable window instance
     */
    @SuppressWarnings("unchecked")
    public static <T extends NavigableWindow> T getCurrentNavigableWindow() {
        return (T) currentNavigableWindow.get();
    }

    /**
     * @return application navigation manager
     */
    public NavigationManager getNavigationManager() {
        return navigationManager;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void transactionStart(Application application, Object transactionData) {
        if (this != application) {
            return;
        }

        if (getCurrent() == null) {
            currentApplication.set(this);
        }

        // SEE: http://vaadin.com/forum/-/message_boards/message/57240
        //   Probably to be removed with Vaadin 7 when the uri mechanism will be more deeply integrated in the core of Vaadin.
        // This tries to detect if it's an initial request (HomeScreen should be rendered) or not.
        // If we don't do that, the HomeScreen is redisplayed before every page when we use a direct url (or a link).
        // Better solution should come with Vaadin 7.
        HttpServletRequest request = (HttpServletRequest) transactionData;
        initialUriFragment.set(request.getParameter("fr"));  // Could be null.
        currentRequest.set(request);

        if (servletContext == null) {
            servletContext = (request.getContextPath().equals("/") ? "/" : request.getContextPath()) + "/";
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void transactionEnd(Application application, Object transactionData) {
        if (this != application) {
            return;
        }

        currentApplication.remove();
        currentNavigableWindow.remove();
        initialUriFragment.remove();
        currentRequest.remove();
    }

    /**
     * The same concept as RoR and Grails flash scope.
     * Provides storage for temporal data which will be available for 2 requests
     *
     * @return navigation flash memory object
     */
    public NavigationFlash getFlash() {
        return flash;
    }

    /**
     * {@inheritDoc}
     * Calls current window to detach: this will allow developer to put
     * some cleanup logic into page detach() method.
     * Note: if you need to override this method, don't forget to call super()
     */
    @Override
    public void close() {
        if (getCurrentNavigableWindow() != null) {
            // calling detach to ensure that detach will be called fro currently attached page
            // (page may release resources here or remove itself from e.g. some global event bus)
            ((Window) getCurrentNavigableWindow()).detach();
        }
        super.close();
    }

    /**
     * @return current http request
     */
    public static HttpServletRequest getCurrentRequest() {
        return currentRequest.get();
    }

    /**
     * @return application servlet context
     */
    public String getServletContext() {
        return servletContext;
    }
}
